/*   
 *  Copyright 1999-2004 The Apache Sofware Foundation.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.tomcat.modules.session;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Random;

/**
 * Generate session IDs. Will use a random generator and the load balancing
 * route.
 * 
 * This class generates a unique 10+ character id. This is good for
 * authenticating users or tracking users around.
 * 
 * 
 * This code was borrowed from Apache JServ.JServServletManager.java. * It is
 * what Apache JServ uses to generate session ids for users. * Unfortunately, it
 * was not included in Apache JServ as a class * so I had to create one here in
 * order to use it. * *
 * 
 * @author costin@eng.sun.com *
 * @author hans@gefionsoftware.com *
 * @author pfrieden@dChain.com *
 * @author Shai Fultheim [shai@brm.com] *
 * @author James Duncan Davidson [duncan@eng.sun.com] *
 * @author Jason Hunter [jhunter@acm.org] *
 * @author Jon S. Stevens jon@latchkey.com
 */
public final class SessionIdGenerator {
	String randomClassName = null;
	Random randomSource = null;
	DataInputStream randomIS = null;
	String devRandomSource = "/dev/urandom";

	public SessionIdGenerator() {
	}

	// Configuration properties

	public final void setRandomClass(String randomClass) {
		this.randomClassName = randomClass;
	}

	/**
	 * Use /dev/random-type special device. This is new code, but may reduce the
	 * big delay in generating the random.
	 * 
	 * You must specify a path to a random generator file. Use /dev/urandom for
	 * linux ( or similar ) systems. Use /dev/random for maximum security ( it
	 * may block if not enough "random" exist ). You can also use a pipe that
	 * generates random.
	 * 
	 * The code will check if the file exists, and default to java Random if not
	 * found. There is a significant performance difference, very visible on the
	 * first call to getSession ( like in the first JSP ) - so use it if
	 * available.
	 */
	public void setRandomFile(String s) {
		// as a hack, you can use a static file - and genarate the same
		// session ids ( good for strange debugging )
		try {
			devRandomSource = s;
			File f = new File(devRandomSource);
			if (!f.exists())
				return;
			randomIS = new DataInputStream(new FileInputStream(f));
			randomIS.readLong();
		} catch (IOException ex) {
			randomIS = null;
		}
	}

	// Internal methods
	String createNewId(String jsIdent) {
		/**
		 * When using a SecurityManager and a JSP page or servlet triggers
		 * creation of a new session id it must be performed with the
		 * Permissions of this class using doPriviledged because the parent JSP
		 * or servlet may not have sufficient Permissions.
		 */
		String newId = null;

		if (System.getSecurityManager() == null) {
			newId = getIdentifier(jsIdent);
			return newId;
		}
		return newId;
	}

	/**
	 * Create the random generator using the configured class name, and
	 * defaulting to java.security.SecureRandom
	 */
	private void createRandomClass() {
		if (randomClassName == null) {
			// set a reasonable default
			if (randomClassName == null) {
				randomClassName = "java.security.SecureRandom";
			}
		}
		try {
			Class randomClass = Class.forName(randomClassName);
			randomSource = (java.util.Random) randomClass.newInstance();
		} catch (Exception e) {
		}
		if (randomSource == null)
			randomSource = new java.security.SecureRandom();
	}

	/*
	 * Create a suitable string for session identification Use synchronized
	 * count and time to ensure uniqueness. Use random string to ensure
	 * timestamp cannot be guessed by programmed attack.
	 * 
	 * format of id is <6 chars random><3 chars time><1+ char count>
	 */
	static private int session_count = 0;
	static private long lastTimeVal = 0;

	// MAX_RADIX is 36
	/*
	 * we want to have a random string with a length of 6 characters. Since we
	 * encode it BASE 36, we've to modulo it with the following value:
	 */
	public final static long maxRandomLen = 2176782336L; // 36 ** 6

	/*
	 * The session identifier must be unique within the typical lifespan of a
	 * Session, the value can roll over after that. 3 characters: (this means a
	 * roll over after over an day which is much larger than a typical lifespan)
	 */
	public final static long maxSessionLifespanTics = 46656; // 36 ** 3

	/*
	 * millisecons between different tics. So this means that the 3-character
	 * time string has a new value every 2 seconds:
	 */
	public final static long ticDifference = 2000;

	// ** NOTE that this must work together with get_jserv_session_balance()
	// ** in jserv_balance.c
	public synchronized String getIdentifier(String jsIdent) {
		StringBuffer sessionId = new StringBuffer();

		// random value ..
		long n = 0;
		if (randomIS != null) {
			try {
				n = randomIS.readLong();
				// System.out.println("Getting /dev/random " + n );
			} catch (IOException ex) {
				ex.printStackTrace();
				randomIS = null;
				// We could also re-open it ( if it's a file of random values )
			}
		}
		if (randomIS == null) {
			if (randomSource == null)
				createRandomClass();
			n = randomSource.nextLong();
		}

		if (n < 0)
			n = -n;
		n %= maxRandomLen;
		// add maxLen to pad the leading characters with '0'; remove
		// first digit with substring.
		n += maxRandomLen;
		sessionId.append(Long.toString(n, Character.MAX_RADIX).substring(1));

		long timeVal = (System.currentTimeMillis() / ticDifference);
		// cut..
		timeVal %= maxSessionLifespanTics;
		// padding, see above
		timeVal += maxSessionLifespanTics;

		sessionId.append(Long.toString(timeVal, Character.MAX_RADIX).substring(
				1));

		/*
		 * make the string unique: append the session count since last time
		 * flip.
		 */
		// count sessions only within tics. So the 'real' session count
		// isn't exposed to the public ..
		if (lastTimeVal != timeVal) {
			lastTimeVal = timeVal;
			session_count = 0;
		}
		sessionId.append(Long.toString(++session_count, Character.MAX_RADIX));

		if (jsIdent != null && jsIdent.length() > 0) {
			return sessionId.toString() + "." + jsIdent;
		}
		return sessionId.toString();
	}

}
